package cn.texous.util.commons.util.encrypt.tea;

import cn.texous.util.commons.util.encrypt.HexStringUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.UnsupportedEncodingException;

/**
 * Implementation of the Tiny Encryption Algorithm (TEA).
 * The Tiny Encryption Algorithm is one of the fastest and most efficient
 * cryptographic algorithms in existence. It was developed by David Wheeler and
 * Roger Needham at the Computer Laboratory of Cambridge University.
 *
 * <p>See http://www.cl.cam.ac.uk/ftp/users/djw3/tea.ps
 *
 * <p>This software was written to provide simple encryption for J2ME.
 * The homepage for this software is http://winterwell.com/software/TEA.php
 *
 * <p>(c) 2008 Joe Halliwell &lt;joe.halliwell@gmail.com&gt;
 *
 * <p>This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * <p>This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * <p>You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;
 *
 * <p>加密解密增加向量 IV， 向量IV 是一个 8byte 长度的 16 进制字符串.
 */
public class TEABaseIvNoLen {

    private static final int SUGAR = 0x9E3779B9;
    private static final int DEF_CUPS = 32;
    private static final int UNSUGAR = 0xC6EF3720;

    private static int CUPS = DEF_CUPS;

    private static int[] KEY = new int[4];
    private static int[] IV = new int[2];

    /**
     * Initialize the cipher for encryption or decryption.
     *
     * @param key a 16 byte (128-bit) key
     */
    public TEABaseIvNoLen(byte[] key, byte[] iv, Integer cups) {
        if (key == null)
            throw new RuntimeException("Invalid key: Key was null");
        if (key.length < 16)
            throw new RuntimeException("Invalid key: Length was less than 16 bytes");
        for (int off = 0, i = 0; i < 4; i++) {
            KEY[i] = ((key[off++] & 0xff))
                    | ((key[off++] & 0xff) << 8)
                    | ((key[off++] & 0xff) << 16)
                    | ((key[off++] & 0xff) << 24);
        }

        if (iv == null)
            throw new RuntimeException("Invalid iv: Key was null");
        if (iv.length < 8)
            throw new RuntimeException("Invalid iv: Length was less than 8 bytes");
        for (int off = 0, i = 0; i < 2; i++) {
            IV[i] = ((iv[off++] & 0xff))
                    | ((iv[off++] & 0xff) << 8)
                    | ((iv[off++] & 0xff) << 16)
                    | ((iv[off++] & 0xff) << 24);
        }

        if (cups != null)
            CUPS = cups;

    }

    /**
     * Initialize the cipher for encryption or decryption.
     *
     * @param key a 16 byte (128-bit) key
     */
    public TEABaseIvNoLen(byte[] key, byte[] iv) {
        this(key, iv, null);
    }

    public TEABaseIvNoLen(String key, String iv, Integer cups) {
        if (key == null)
            throw new RuntimeException("Invalid key: Key was null");
        if (key.length() < 32)
            throw new RuntimeException("Invalid key: Length was less than 16 bytes");
        KEY = HexStringUtils.convertFromHexStringToInts(key);
        if (iv == null)
            throw new RuntimeException("Invalid key: Key was null");
        if (iv.length() < 16)
            throw new RuntimeException("Invalid key: Length was less than 8 bytes");
        IV = HexStringUtils.convertFromHexStringToInts(iv);
        if (cups != null)
            CUPS = cups;
    }

    public TEABaseIvNoLen(String key, String iv) {
        this(key, iv, null);
    }

    /**
     * Encrypt an array of bytes.
     *
     * @param clear the cleartext to encrypt
     * @return the encrypted text
     */
    public byte[] encrypt(byte[] clear) {
        int paddedSize = ((clear.length / 8) + (((clear.length % 8) == 0) ? 0 : 1)) * 2;
        int[] buffer = new int[paddedSize];
        pack(clear, buffer, 0);
        brew(buffer);
        return unpack(buffer, 0, buffer.length * 4);
    }

    /**
     * Decrypt an array of bytes.
     *
     * @param crypt the cipher text to decrypt
     * @return the decrypted text
     */
    public byte[] decrypt(byte[] crypt) {
        assert crypt.length % 4 == 0;
        assert (crypt.length / 4) % 2 == 1;
        int[] buffer = new int[crypt.length / 4];
        pack(crypt, buffer, 0);
        unbrew(buffer);
        return unpack(buffer, 0, buffer.length * 4);
    }

    void brew(int[] buf) {
        assert buf.length % 2 == 1;
        int i = 0;
        int v0;
        int v1;
        int sum;
        int n;
        int iv0 = IV[0];
        int iv1 = IV[1];
        while (i < buf.length) {
            n = CUPS;
            v0 = buf[i] ^ iv0;
            v1 = buf[i + 1] ^ iv1;
            sum = 0;
            while (n-- > 0) {
                sum += SUGAR;
                v0 += ((v1 << 4) + KEY[0]) ^ (v1 + sum) ^ ((v1 >> 5) + KEY[1]);
                v1 += ((v0 << 4) + KEY[2]) ^ (v0 + sum) ^ ((v0 >> 5) + KEY[3]);
            }
            buf[i] = v0;
            buf[i + 1] = v1;
            iv0 = v0;
            iv1 = v1;
            i += 2;
        }
    }

    void unbrew(int[] buf) {
        assert buf.length % 2 == 1;
        int i = 0;
        int v0;
        int v1;
        int sum;
        int n;
        int iv0 = IV[0];
        int iv1 = IV[1];
        while (i < buf.length) {

            n = CUPS;
            v0 = buf[i];
            v1 = buf[i + 1];
            sum = UNSUGAR;
            while (n-- > 0) {
                v1 -= ((v0 << 4) + KEY[2]) ^ (v0 + sum) ^ ((v0 >> 5) + KEY[3]);
                v0 -= ((v1 << 4) + KEY[0]) ^ (v1 + sum) ^ ((v1 >> 5) + KEY[1]);
                sum -= SUGAR;
            }
            int tempIv0 = iv0;
            iv0 = buf[i];
            buf[i] = v0 ^ tempIv0;

            int tempIv1 = iv1;
            iv1 = buf[i + 1];
            buf[i + 1] = v1 ^ tempIv1;
            i += 2;
        }
    }

    void pack(byte[] src, int[] dest, int destOffset) {
        assert destOffset + (src.length / 4) <= dest.length;
        int i = 0;
        int shift = 24;
        int j = destOffset;
        dest[j] = 0;
        while (i < src.length) {
            dest[j] |= (src[i] & 0xff) << shift;
            if (shift == 0) {
                shift = 24;
                j++;
                if (j < dest.length) dest[j] = 0;
            } else {
                shift -= 8;
            }
            i++;
        }
    }

    byte[] unpack(int[] src, int srcOffset, int destLength) {
        assert destLength <= (src.length - srcOffset) * 4;
        byte[] dest = new byte[destLength];
        int i = srcOffset;
        int count = 0;
        for (int j = 0; j < destLength; j++) {
            dest[j] = (byte) ((src[i] >> (24 - (8 * count))) & 0xff);
            count++;
            if (count == 4) {
                count = 0;
                i++;
            }
        }
        return dest;
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
        String clearStr = args[0];
        String keyStr = args[1];
        String ivStr = args[2];
        System.out.println("----has iv not length");
        TEABaseIvNoLen teaBaseIvNoLen = new TEABaseIvNoLen(keyStr, ivStr);
        byte[] clearIvNlByte = clearStr.getBytes("UTF-8");
        byte[] cryptIvNlByte = teaBaseIvNoLen.encrypt(clearIvNlByte);
        String cryptIvNlStr = Base64.encodeBase64String(cryptIvNlByte);
        System.out.println("----cryptIvNLStr: " + cryptIvNlStr);
        byte[] cIvNlByte = teaBaseIvNoLen.decrypt(cryptIvNlByte);
        System.out.println("----clearIvNLStr: " + new String(cIvNlByte));
    }

}
